package plc

import (
	"encoding/binary"
	"fmt"
	"github.com/robinson/gos7"
	"time"
	_ "unsafe"
)

// hock==============================================================================================
const (
	// Area ID
	s7areape = 0x81 //process inputs
	s7areapa = 0x82 //process outputs
	s7areamk = 0x83 //Merkers
	s7areadb = 0x84 //DB
	s7areact = 0x1C //counters
	s7areatm = 0x1D //timers

	// Word Length
	s7wlbit     = 0x01 //Bit (inside a word)
	s7wlbyte    = 0x02 //Byte (8 bit)
	s7wlChar    = 0x03
	s7wlword    = 0x04 //Word (16 bit)
	s7wlint     = 0x05
	s7wldword   = 0x06 //Double Word (32 bit)
	s7wldint    = 0x07
	s7wlreal    = 0x08 //Real (32 bit float)
	s7wlcounter = 0x1C //Counter (16 bit)
	s7wltimer   = 0x1D //Timer (16 bit)

	// PLC Status
	s7CpuStatusUnknown = 0
	s7CpuStatusRun     = 8
	s7CpuStatusStop    = 4

	//size header
	sizeHeaderRead  int = 31 // Header Size when Reading
	sizeHeaderWrite int = 35 // Header Size when Writing

	// Result transport size
	tsResBit   = 3
	tsResByte  = 4
	tsResInt   = 5
	tsResReal  = 7
	tsResOctet = 9
)
const (
	errIsoInvalidPDU      = 0x00030000 // Bad format
	errIsoInvalidDataSize = 0x00040000 // Bad Datasize passed to send/recv : buffer is invalid
)

var s7ReadWriteTelegram = []byte{ // 31-35 bytes
	3, 0,
	0, 31, // Telegram Length (Data Size + 31 or 35)
	2, 240, 128, // COTP (see above for info)
	50,   // S7 Protocol ID
	1,    // Job Type
	0, 0, // Redundancy identification
	5, 0, // PDU Reference //lth this use for request S7 packet id
	0, 14, // Parameters Length
	0, 0, // Data Length = Size(bytes) + 4
	4,              // Function 4 Read Var, 5 Write Var
	1,              // Items count
	18,             // Var spec.
	10,             // Length of remaining bytes
	16,             // Syntax ID
	byte(s7wlbyte), // Transport Size idx=22
	0, 0,           // Num Elements
	0, 0, // DB Number (if any, else 0)
	132,     // Area Type
	0, 0, 0, // Area Offset
	// WR area
	0,    // Reserved
	4,    // Transport size
	0, 0} // Data Length * 8 (if not bit or timer or counter)

type Client struct {
	gos7.Client
	*gos7.TCPClientHandler
}

func NewClient(address string, rack, slot int) (*Client, error) {
	handler := gos7.NewTCPClientHandler(address, rack, slot)
	handler.Timeout = 10 * time.Second
	handler.IdleTimeout = 0
	//handler.Logger = log.New(os.Stdout, "tcp: ", log.LstdFlags)
	tcpErr := handler.Connect()
	if tcpErr != nil {
		return nil, tcpErr
	}
	cl := gos7.NewClient(handler)
	return &Client{Client: cl, TCPClientHandler: handler}, nil
}

func (mb *Client) AGReadV(start int, size int, buffer []byte) (err error) {
	return mb.AGReadDB(1, start, size, buffer)
}

func (mb *Client) AGWriteV(start int, size int, buffer []byte) (err error) {
	return mb.AGWriteDB(1, start, size, buffer)
}

func (mb *Client) AGReadM(start int, size int, buffer []byte) (err error) {
	return mb.AGReadMB(start, size, buffer)
}

func (mb *Client) AGWriteM(start int, size int, buffer []byte) (err error) {
	return mb.AGWriteMB(start, size, buffer)
}

func (mb *Client) AGReadI(start int, size int, buffer []byte) (err error) {
	return mb.AGReadEB(start, size, buffer)
}

func (mb *Client) AGWriteI(start int, size int, buffer []byte) (err error) {
	return mb.AGWriteEB(start, size, buffer)
}

func (mb *Client) AGReadQ(start int, size int, buffer []byte) (err error) {
	return mb.AGReadAB(start, size, buffer)
}

func (mb *Client) AGWriteQ(start int, size int, buffer []byte) (err error) {
	return mb.AGWriteAB(start, size, buffer)
}

func (mb *Client) BitWriteDB(dbNumber, start, mBit, size int, buffer []byte) (err error) {
	address := (start << 3) + mBit
	return mb.writeArea(s7areadb, dbNumber, address, size, s7wlbit, buffer)
}
func (mb *Client) BitWriteM(start, mBit, size int, buffer []byte) (err error) {
	address := (start << 3) + mBit
	return mb.writeArea(s7areamk, 0, address, size, s7wlbit, buffer)
}
func (mb *Client) BitWriteQ(start, mBit, size int, buffer []byte) (err error) {
	address := (start << 3) + mBit
	return mb.writeArea(s7areapa, 0, address, size, s7wlbit, buffer)
}
func (mb *Client) BitWriteI(start, mBit, size int, buffer []byte) (err error) {
	address := (start << 3) + mBit
	return mb.writeArea(s7areape, 0, address, size, s7wlbit, buffer)
}
func (mb *Client) BitWriteV(start, mBit, size int, buffer []byte) (err error) {
	address := (start << 3) + mBit
	return mb.writeArea(s7areadb, 1, address, size, s7wlbit, buffer)
}

func (mb *Client) Close() error {
	return mb.TCPClientHandler.Close()
}

//go:linkname dataSizeByte github.com/robinson/gos7.dataSizeByte
func dataSizeByte(wordLength int) int

//go:linkname responseError github.com/robinson/gos7.responseError
func responseError(response *gos7.ProtocolDataUnit) error

func (mb *Client) send(request *gos7.ProtocolDataUnit) (response *gos7.ProtocolDataUnit, err error) {
	dataResponse, err := mb.TCPClientHandler.Send(request.Data)
	if err != nil {
		return
	}

	if err = mb.TCPClientHandler.Verify(request.Data, dataResponse); err != nil {
		return
	}
	if dataResponse == nil || len(dataResponse) == 0 {
		// Empty response
		err = fmt.Errorf("s7: response data is empty")
		return
	}
	response = &gos7.ProtocolDataUnit{
		Data: dataResponse,
	}
	//check for error if any
	err = responseError(response)
	return response, err
}

func (mb *Client) writeArea(area int, dbnumber int, start int, amount int, wordlen int, buffer []byte) (err error) {
	var address, numElements, maxElements, totElements, dataSize, isoSize, length int
	offset := 0
	wordSize := 1

	// Some adjustment
	if area == s7areact {
		wordlen = s7wlcounter
	}
	if area == s7areatm {
		wordlen = s7wltimer
	}

	// Calc Word size
	wordSize = dataSizeByte(wordlen)
	if wordSize == 0 {
		return fmt.Errorf(gos7.ErrorText(errIsoInvalidDataSize))
	}

	if wordlen == s7wlbit {
		amount = 1 // Only 1 bit can be transferred at time
	} else {
		if wordlen != s7wlcounter && wordlen != s7wltimer {
			amount = amount * wordSize
			wordSize = 1
			wordlen = s7wlbyte
		}
	}
	tt, _ := interface{}(mb.TCPClientHandler).(*gos7.TCPClientHandler)
	maxElements = (tt.PDULength - 35) / wordSize // 35 = Reply telegram header
	totElements = amount
	for totElements > 0 && err == nil {
		numElements = totElements
		if numElements > maxElements {
			numElements = maxElements
		}
		dataSize = numElements * wordSize
		isoSize = sizeHeaderWrite + dataSize

		// Setup the telegram
		requestData := make([]byte, sizeHeaderWrite)
		copy(requestData[0:], s7ReadWriteTelegram[0:])

		request := gos7.NewProtocolDataUnit(requestData)
		// Whole telegram Size
		binary.BigEndian.PutUint16(request.Data[2:], uint16(isoSize))
		//SetWordAt(request.Data, 2, uint16(isoSize))
		// Data length
		length = dataSize + 4
		binary.BigEndian.PutUint16(request.Data[15:], uint16(length))
		// SetWordAt(request.Data, 15, uint16(length))
		// Function
		request.Data[17] = byte(0x05)
		// Set DB Number
		request.Data[27] = byte(area)
		if area == s7areadb {
			binary.BigEndian.PutUint16(request.Data[25:], uint16(dbnumber))
			//SetWordAt(request.Data, 25, uint16(dbnumber))
		}
		// Adjusts start and word length
		if wordlen == s7wlbit || wordlen == s7wlcounter || wordlen == s7wltimer {
			address = start
			length = dataSize
			request.Data[22] = byte(wordlen)
		} else {
			address = start << 3
			length = dataSize << 3
		}
		// Num elements
		binary.BigEndian.PutUint16(request.Data[23:], uint16(numElements))
		// SetWordAt(request.Data, 23, uint16(numElements))
		// address into the PLC
		request.Data[30] = byte(address & 0x0FF)
		address = address >> 8
		request.Data[29] = byte(address & 0x0FF)
		address = address >> 8
		request.Data[28] = byte(address & 0x0FF)

		// Transport Size
		switch wordlen {
		case s7wlbit:
			request.Data[32] = tsResBit
			break
		case s7wlcounter:
		case s7wltimer:
			request.Data[32] = tsResOctet
			break
		default:
			request.Data[32] = tsResByte // byte/word/dword etc.
			break
		}
		// length
		// SetWordAt(request.Data, 33, uint16(length))
		binary.BigEndian.PutUint16(request.Data[33:], uint16(length))
		//expand values into array
		request.Data = append(request.Data[:35], append(buffer[offset:offset+dataSize], request.Data[35:]...)...)
		response, sendError := mb.send(&request)
		err = sendError
		if err == nil {
			if length = len(response.Data); length == 22 {
				if response.Data[21] != byte(0xFF) {
					err = fmt.Errorf(gos7.ErrorText(gos7.CPUError(uint(response.Data[21]))))
				}
			} else {
				err = fmt.Errorf(gos7.ErrorText(errIsoInvalidPDU))
			}
		}
		offset += dataSize
		totElements -= numElements
		start += numElements * wordSize
	}
	return
}
